// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.linqingying.gitcode

import com.intellij.dvcs.DvcsUtil
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.vcs.VcsException
import com.intellij.util.containers.ContainerUtil
import com.linqingying.gitcode.api.GCRepositoryPath
import com.linqingying.gitcode.api.GitCodeApiRequestExecutor
import com.linqingying.gitcode.api.GitCodeApiRequests.Repos.get
import com.linqingying.gitcode.api.data.GitCodeRepo
import com.linqingying.gitcode.authentication.accounts.GCAccountManager
import com.linqingying.gitcode.authentication.accounts.GitCodeAccount
import com.linqingying.gitcode.authentication.ui.GitCodeChooseAccountDialog
import com.linqingying.gitcode.i18n.GitCodeBundle
import com.linqingying.gitcode.util.*
import com.linqingying.gitcode.util.GCCompatibilityUtil.getOrRequestToken
import com.linqingying.gitcode.util.GCCompatibilityUtil.requestNewAccountForServer
import git4idea.DialogManager
import git4idea.GitActivity.Rebase
import git4idea.commands.Git
import git4idea.config.GitVcsSettings
import git4idea.fetch.GitFetchSupport
import git4idea.i18n.GitBundle
import git4idea.rebase.GitRebaser
import git4idea.remote.hosting.GitHostingUrlUtil.match
import git4idea.remote.hosting.knownRepositories
import git4idea.repo.GitRemote
import git4idea.repo.GitRepository
import git4idea.repo.GitRepositoryManager
import git4idea.update.GitUpdateResult
import git4idea.util.GitPreservingProcess
import java.io.IOException
import java.util.*

class GitCodeSyncForkAction : DumbAwareAction() {
    override fun getActionUpdateThread(): ActionUpdateThread {
        return ActionUpdateThread.BGT
    }

    override fun update(e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = isEnabledAndVisible(e)
    }

    override fun actionPerformed(e: AnActionEvent) {
        FileDocumentManager.getInstance().saveAllDocuments()

        val project = Objects.requireNonNull(e.getData(CommonDataKeys.PROJECT))

        val gitRepositoryManager = project?.getServiceIfCreated(
            GitRepositoryManager::class.java
        )
        if (gitRepositoryManager == null) {
            LOG.warn("Unable to get the GitRepositoryManager service")
            return
        }

        if (gitRepositoryManager.repositories.size > 1) {
            project.let {
                GitCodeNotifications.showError(
                    it,
                    GitCodeNotificationIdsHolder.REBASE_MULTI_REPO_NOT_SUPPORTED,
                    GitCodeBundle.message("rebase.error"),
                    GitCodeBundle.message("rebase.error.multi.repo.not.supported")
                )
            }
            return
        }

        val ghRepositoriesManager = project.getServiceIfCreated(
            GCHostedRepositoriesManager::class.java
        )
        if (ghRepositoriesManager == null) {
            LOG.warn("Unable to get the GCProjectRepositoriesManager service")
            return
        }

        val repositories = ghRepositoriesManager.knownRepositories
        val originMapping = ContainerUtil.find(
            repositories
        ) { mapping: GCGitRepositoryMapping -> mapping.remote.remote.name == ORIGIN_REMOTE_NAME }
        if (originMapping == null) {
            GitCodeNotifications.showError(
                project,
                GitCodeNotificationIdsHolder.REBASE_REMOTE_ORIGIN_NOT_FOUND,
                GitCodeBundle.message("rebase.error"),
                GitCodeBundle.message("rebase.error.remote.origin.not.found")
            )
            return
        }

        val accountManager = ApplicationManager.getApplication().getService(
            GCAccountManager::class.java
        )
        val serverPath = originMapping.repository.serverPath
        val gitcodeAccount: GitCodeAccount?
        val accounts = ContainerUtil.filter(
            accountManager.accountsState.value
        ) { account: GitCodeAccount -> serverPath == account.server }
        gitcodeAccount = if (accounts.size == 0) {
            requestNewAccountForServer(serverPath, project)
        } else if (accounts.size == 1) {
            accounts[0]
        } else {
            val chooseAccountDialog = GitCodeChooseAccountDialog(
                project,
                null,
                accounts,
                GitCodeBundle.message("account.choose.for", serverPath),
                false,
                true
            )
            DialogManager.show(chooseAccountDialog)
            if (chooseAccountDialog.isOK) {
                chooseAccountDialog.account
            } else {
                GitCodeNotifications.showError(
                    project,
                    GitCodeNotificationIdsHolder.REBASE_ACCOUNT_NOT_FOUND,
                    GitCodeBundle.message("rebase.error"),
                    GitCodeBundle.message("rebase.error.no.suitable.account.found")
                )
                return
            }
        }
        if (gitcodeAccount == null) {
            GitCodeNotifications.showError(
                project,
                GitCodeNotificationIdsHolder.REBASE_ACCOUNT_NOT_FOUND,
                GitCodeBundle.message("rebase.error"),
                GitCodeBundle.message("rebase.error.no.suitable.account.found")
            )
            return
        }

        SyncForkTask(
            project, Git.getInstance(), gitcodeAccount,
            originMapping.remote.repository,
            originMapping.repository.repositoryPath
        ).queue()
    }

    private class SyncForkTask(
        project: Project,
        private val myGit: Git,
        private val myAccount: GitCodeAccount,
        private val myRepository: GitRepository,
        private val myRepoPath: GCRepositoryPath
    ) :
        Task.Backgroundable(project, GitCodeBundle.message("rebase.process")) {
        override fun run(indicator: ProgressIndicator) {
            val token = getOrRequestToken(myAccount, myProject!!) ?: return
            val executor = GitCodeApiRequestExecutor.Factory.getInstance().create(myAccount.server, token)

            myRepository.update()

            val parentRepo = validateRepoAndLoadParent(executor, indicator) ?: return

            val parentRemote = configureParentRemote(indicator, parentRepo.fullPath) ?: return

            val branchName = parentRepo.defaultBranch
            if (branchName == null) {
                GitCodeNotifications.showError(
                    myProject,
                    GitCodeNotificationIdsHolder.REBASE_REPO_NOT_FOUND,
                    GitCodeBundle.message("rebase.error"),
                    GitCodeBundle.message("rebase.error.no.default.branch")
                )
                return
            }

            if (!fetchParent(indicator, parentRemote)) {
                return
            }

            rebaseCurrentBranch(indicator, parentRemote, branchName)
        }

        fun validateRepoAndLoadParent(executor: GitCodeApiRequestExecutor, indicator: ProgressIndicator): GitCodeRepo? {
            try {
                val repositoryInfo =
                    executor.execute(
                        indicator,
                        get(myAccount.server, myRepoPath.owner, myRepoPath.repository)
                    )
                if (repositoryInfo == null) {
                    GitCodeNotifications.showError(
                        myProject!!,
                        GitCodeNotificationIdsHolder.REBASE_REPO_NOT_FOUND,
                        GitCodeBundle.message("rebase.error"),
                        GitCodeBundle.message("rebase.error.repo.not.found", myRepoPath.toString())
                    )
                    return null
                }

                val parentRepo = repositoryInfo.parent
                if (!repositoryInfo.isFork || parentRepo == null) {
                    GitCodeNotifications.showWarningURL(
                        myProject!!,
                        GitCodeNotificationIdsHolder.REBASE_REPO_IS_NOT_A_FORK,
                        GitCodeBundle.message("rebase.error"),
                        "GitCode repository ", "'" + repositoryInfo.name + "'", " is not a fork",
                        repositoryInfo.webUrl
                    )
                    return null
                }
                return parentRepo
            } catch (e: IOException) {
                GitCodeNotifications.showError(
                    myProject!!,
                    GitCodeNotificationIdsHolder.REBASE_CANNOT_LOAD_REPO_INFO,
                    GitCodeBundle.message("cannot.load.repo.info"),
                    e
                )
                return null
            }
        }

        fun configureParentRemote(indicator: ProgressIndicator, parentRepoPath: GCRepositoryPath): GitRemote? {
            LOG.info("Configuring upstream remote")
            indicator.text = GitCodeBundle.message("rebase.process.configuring.upstream.remote")

            var upstreamRemote = findRemote(parentRepoPath)
            if (upstreamRemote != null) {
                LOG.info("Correct upstream remote already exists")
                return upstreamRemote
            }

            LOG.info("Adding GitCode parent as a remote host")
            indicator.text = GitCodeBundle.message("rebase.process.adding.gitcode.parent.as.remote.host")
            val parentRepoUrl = GitCodeGitHelper.getInstance().getRemoteUrl(myAccount.server, parentRepoPath)
            try {
                myGit.addRemote(myRepository, UPSTREAM_REMOTE_NAME, parentRepoUrl).throwOnError()
            } catch (e: VcsException) {
                GitCodeNotifications.showError(
                    myProject!!,
                    GitCodeNotificationIdsHolder.REBASE_CANNOT_CONFIGURE_UPSTREAM_REMOTE,
                    GitCodeBundle.message("rebase.error"),
                    GitCodeBundle.message("cannot.configure.remote", UPSTREAM_REMOTE_NAME, e.message)
                )
                return null
            }
            myRepository.update()
            upstreamRemote = findRemote(parentRepoPath)
            if (upstreamRemote == null) {
                GitCodeNotifications.showError(
                    myProject!!,
                    GitCodeNotificationIdsHolder.REBASE_CANNOT_CONFIGURE_UPSTREAM_REMOTE,
                    GitCodeBundle.message("rebase.error"),
                    GitCodeBundle.message("rebase.error.upstream.not.found", UPSTREAM_REMOTE_NAME)
                )
            }
            return upstreamRemote
        }

        fun findRemote(repoPath: GCRepositoryPath): GitRemote? {
            return ContainerUtil.find(
                myRepository.remotes
            ) { remote: GitRemote ->
                val url = remote.firstUrl
                if (url == null || !match(myAccount.server.toURI(), url)) return@find false

                val remotePath = GitCodeUrlUtil.getUserAndRepositoryFromRemoteUrl(url)
                repoPath.equals(remotePath)
            }
        }

        fun fetchParent(indicator: ProgressIndicator, remote: GitRemote): Boolean {
            LOG.info("Fetching upstream")
            indicator.text = GitCodeBundle.message("rebase.process.fetching.upstream")
            return GitFetchSupport.fetchSupport(myProject!!).fetch(myRepository, remote).showNotificationIfFailed()
        }

        fun rebaseCurrentBranch(
            indicator: ProgressIndicator,
            parentRemote: GitRemote,
            branch: @NlsSafe String
        ) {
            val onto = parentRemote.name + "/" + branch
            LOG.info("Rebasing current branch")
            indicator.text = GitCodeBundle.message("rebase.process.rebasing.branch.onto", onto)
            DvcsUtil.workingTreeChangeStarted(myProject!!, GitBundle.message("activity.name.rebase"), Rebase)
                .use { ignore ->
                    val rootsToSave = listOf(myRepository.root)
                    val saveMethod = GitVcsSettings.getInstance(myProject).saveChangesPolicy
                    val process =
                        GitPreservingProcess(
                            project,
                            myGit,
                            rootsToSave,
                            GitCodeBundle.message("rebase.process.operation.title"),
                            onto,
                            saveMethod,
                            indicator
                        ) { doRebaseCurrentBranch(indicator, onto) }
                    process.execute()
                }
        }

        fun doRebaseCurrentBranch(indicator: ProgressIndicator, onto: String) {
            val root = myRepository.root

            val result = GitRebaser(myProject!!, myGit, indicator).rebase(root, listOf(onto))
            myRepository.update()

            if (result == GitUpdateResult.NOTHING_TO_UPDATE || result == GitUpdateResult.SUCCESS || result == GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS) {
                root.refresh(false, true)
                GitCodeNotifications.showInfo(
                    project,
                    GitCodeNotificationIdsHolder.REBASE_SUCCESS,
                    GitCodeBundle.message("rebase.process.success"),
                    ""
                )
            }
        }
    }

    companion object {
        private val LOG = GitCodeUtil.LOG
        private const val UPSTREAM_REMOTE_NAME = "upstream"
        private const val ORIGIN_REMOTE_NAME = "origin"

        private fun isEnabledAndVisible(e: AnActionEvent): Boolean {
            val project = e.getData(CommonDataKeys.PROJECT)
            if (project == null || project.isDefault) return false

            val repositoriesManager = project.getServiceIfCreated(
                GCHostedRepositoriesManager::class.java
            )
            if (repositoriesManager == null) return false

            val repositories = repositoriesManager.knownRepositories
            return !repositories.isEmpty()
        }
    }
}
